<!doctype html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vue列表渲染详解与示例</title>
    <script src="https://unpkg.com/vue@3.4.0"></script>
    <style>
      body {
        font-family: 'Consolas', '微软雅黑';
        max-width: 1200px;
        margin: 0 auto;
        padding: 20px;
        background-color: #f5f5f5;
        line-height: 1.6;
      }
      code {
        font-family: 'Consolas', '微软雅黑';
      }
      .container {
        background: white;
        padding: 20px;
        border-radius: 10px;
        box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
        margin-bottom: 20px;
      }
      h1 {
        color: #2c3e50;
        text-align: center;
        margin-bottom: 30px;
        font-size: 2.5em;
      }
      h2 {
        color: #34495e;
        border-bottom: 3px solid #3498db;
        padding-bottom: 10px;
        margin-top: 30px;
      }
      h3 {
        color: #2980b9;
        margin-top: 25px;
      }
      .demo-section {
        margin: 20px 0;
        padding: 20px;
        border: 1px solid #ddd;
        border-radius: 8px;
        background-color: #f9f9f9;
      }
      .code-block {
        background-color: #2d3748;
        border-radius: 8px;
        padding: 0;
        margin: 15px 0;
        overflow: hidden;
      }
      .code-block textarea {
        height: auto;
        min-height: 250px;
        font-family: 'Consolas', '微软雅黑';
        font-size: 13px;
        padding: 15px;
        border: none;
        border-radius: 0;
        background-color: transparent;
        resize: none;
        line-height: 1.5;
        box-sizing: border-box;
        color: #e2e8f0;
        width: 100%;
        outline: none;
      }
      .item {
        padding: 8px;
        margin: 5px 0;
        background: #f5f5f5;
        border-radius: 4px;
      }
      .student {
        display: flex;
        justify-content: space-between;
        align-items: center;
      }
      .high-score {
        background: #d4edda;
        border-left: 4px solid #28a745;
      }
      .low-score {
        background: #f8d7da;
        border-left: 4px solid #dc3545;
      }
      .medium-score {
        background: #fff3cd;
        border-left: 4px solid #ffc107;
      }
      .user-info {
        background: #d4edda;
        padding: 15px;
        border-radius: 5px;
        border-left: 4px solid #28a745;
        color: #155724;
      }
      .warning {
        background: #fff3cd;
        padding: 15px;
        border-radius: 5px;
        border-left: 4px solid #ffc107;
        color: #856404;
      }
      .error {
        background: #f8d7da;
        padding: 15px;
        border-radius: 5px;
        border-left: 4px solid #dc3545;
        color: #721c24;
      }
      .info {
        background: #d1ecf1;
        padding: 15px;
        border-radius: 5px;
        border-left: 4px solid #17a2b8;
        color: #0c5460;
      }
      button {
        background-color: #3498db;
        color: white;
        padding: 8px 16px;
        border: none;
        border-radius: 4px;
        cursor: pointer;
        font-size: 14px;
        margin: 2px;
        transition: background-color 0.3s;
      }
      button:hover {
        background-color: #2980b9;
      }
      input[type='text'], input[type='number'] {
        padding: 8px 12px;
        border: 1px solid #ddd;
        border-radius: 4px;
        font-size: 14px;
        margin: 5px;
      }
      .output {
        background-color: #ecf0f1;
        padding: 15px;
        border-radius: 5px;
        margin-top: 10px;
        border-left: 4px solid #3498db;
      }
      .highlight {
        background-color: #fff3cd;
        padding: 2px 4px;
        border-radius: 3px;
      }
      .performance-note {
        background: #e8f4fd;
        padding: 15px;
        border-radius: 5px;
        border-left: 4px solid #2196f3;
        margin: 15px 0;
      }
      .category-section {
        margin: 10px 0;
        padding: 10px;
        background: #f8f9fa;
        border-radius: 5px;
      }
      .dynamic-item {
        display: flex;
        justify-content: space-between;
        align-items: center;
        padding: 10px;
        margin: 5px 0;
        background: #e9ecef;
        border-radius: 4px;
        border-left: 3px solid #6c757d;
      }
    </style>
  </head>
  <body>
    <h1>Vue列表渲染详解与示例</h1>

      <!-- 概述 -->
      <div class="container">
        <h2>概述</h2>
        <p>Vue.js的列表渲染是构建动态用户界面的核心功能之一。通过<code>v-for</code>指令，我们可以基于数据源渲染元素列表。主要包括：</p>
        <ul>
          <li>
            <span class="highlight">v-for</span>
            ：基于数组、对象或数字进行循环渲染
          </li>
          <li>
            <span class="highlight">key属性</span>
            ：为每个列表项提供唯一标识，优化渲染性能
          </li>
          <li>
            <span class="highlight">数组变更检测</span>
            ：Vue能够检测数组的变化并更新视图
          </li>
          <li>
            <span class="highlight">嵌套循环</span>
            ：支持多层嵌套的列表渲染
          </li>
          <li>
            <span class="highlight">动态列表操作</span>
            ：添加、删除、排序等列表操作
          </li>
        </ul>
      </div>

      <!-- 1. 基础数组渲染 -->
      <div class="container">
        <h2>1. 基础数组渲染</h2>
        <p>
          <code>v-for</code>
          指令的基本语法是
          <code>item in items</code>
          ，其中
          <code>items</code>
          是源数据数组，
          <code>item</code>
          是被迭代的数组元素的别名。
        </p>

        <div class="code-block">
          <textarea readonly>
<ul>
  <li v-for="fruit in fruits" :key="fruit">{{ fruit }}</li>
</ul>

<!-- 对应的数据 -->
const fruits = ref(['苹果', '香蕉', '橙子', '葡萄'])
          </textarea>
        </div>

        <div class="demo-section" id="basic-array-demo">
          <h3>实际演示</h3>
          <ul>
            <li v-for="fruit in fruits" :key="fruit">{{ fruit }}</li>
          </ul>
          <div class="output">
            <strong>数组内容：</strong>
            {{ fruits }}
            <br>
            <strong>数组长度：</strong>
            {{ fruits.length }}
          </div>
        </div>
      </div>

      <!-- 2. 带索引的数组渲染 -->
      <div class="container">
        <h2>2. 带索引的数组渲染</h2>
        <p>
          <code>v-for</code>
          还支持可选的第二个参数，即当前项的索引。语法为
          <code>(item, index) in items</code>
          。
        </p>

        <div class="code-block">
          <textarea readonly>
<div v-for="(color, index) in colors" :key="index" class="item">
  {{ index + 1 }}. {{ color }}
</div>

<!-- 对应的数据 -->
const colors = ref(['红色', '绿色', '蓝色', '黄色'])
          </textarea>
        </div>

        <div class="demo-section" id="indexed-array-demo">
          <h3>实际演示</h3>
          <div v-for="(color, index) in colors" :key="index" class="item">
            {{ index + 1 }}. {{ color }}
          </div>
          <div class="output">
            <strong>索引说明：</strong>
            索引从0开始，显示时通常加1以符合用户习惯
          </div>
        </div>
      </div>

      <!-- 3. 对象数组渲染 -->
      <div class="container">
        <h2>3. 对象数组渲染</h2>
        <p>
          在实际应用中，我们经常需要渲染对象数组。每个对象通常包含多个属性，我们可以在模板中访问这些属性。
        </p>

        <div class="code-block">
          <textarea readonly>
<div
  v-for="student in students"
  :key="student.id"
  class="student item"
  :class="{
    'high-score': student.score >= 80,
    'medium-score': student.score >= 60 && student.score < 80,
    'low-score': student.score < 60
  }"
>
  <span>{{ student.name }} (ID: {{ student.id }})</span>
  <span>分数: {{ student.score }}</span>
  <button @click="updateScore(student.id)">+10分</button>
</div>

<!-- 对应的数据 -->
const students = ref([
  { id: 1, name: '张三', score: 85 },
  { id: 2, name: '李四', score: 92 },
  { id: 3, name: '王五', score: 78 },
  { id: 4, name: '赵六', score: 55 }
])
          </textarea>
        </div>

        <div class="demo-section" id="object-array-demo">
          <h3>实际演示</h3>
          <div
            v-for="student in students"
            :key="student.id"
            class="student item"
            :class="{
              'high-score': student.score >= 80,
              'medium-score': student.score >= 60 && student.score < 80,
              'low-score': student.score < 60
            }"
          >
            <span>{{ student.name }} (ID: {{ student.id }})</span>
            <span>分数: {{ student.score }}</span>
            <button @click="updateScore(student.id)">+10分</button>
          </div>
          <div class="output">
            <strong>说明：</strong>
            根据分数自动应用不同的样式类
            <br>
            <strong>绿色：</strong>
            优秀(≥80分) |
            <strong>黄色：</strong>
            良好(60-79分) |
            <strong>红色：</strong>
            需要努力(<60分)
          </div>
        </div>
      </div>

      <!-- 4. 对象属性渲染 -->
      <div class="container">
        <h2>4. 对象属性渲染</h2>
        <p>
          <code>v-for</code>
          也可以用来遍历一个对象的属性。语法为
          <code>(value, key) in object</code>
          ，还可以用第三个参数获取索引
          <code>(value, key, index) in object</code>
          。
        </p>

        <div class="code-block">
          <textarea readonly>
<div v-for="(value, key) in userInfo" :key="key" class="item">
  <strong>{{ key }}:</strong> {{ value }}
</div>

<!-- 也可以获取索引 -->
<div v-for="(value, key, index) in userInfo" :key="key" class="item">
  {{ index + 1 }}. <strong>{{ key }}:</strong> {{ value }}
</div>

<!-- 对应的数据 -->
const userInfo = ref({
  姓名: '张三',
  年龄: 25,
  职业: '前端开发',
  城市: '北京'
})
          </textarea>
        </div>

        <div class="demo-section" id="object-props-demo">
          <h3>实际演示</h3>
          <h4>基础对象遍历：</h4>
          <div v-for="(value, key) in userInfo" :key="key" class="item">
            <strong>{{ key }}:</strong> {{ value }}
          </div>
          
          <h4>带索引的对象遍历：</h4>
          <div v-for="(value, key, index) in userInfo" :key="'indexed-' + key" class="item">
            {{ index + 1 }}. <strong>{{ key }}:</strong> {{ value }}
          </div>
          
          <div class="output">
            <strong>注意：</strong>
            对象属性的遍历顺序在不同JavaScript引擎中可能不同
          </div>
        </div>
      </div>

      <!-- 5. 数字范围渲染 -->
      <div class="container">
        <h2>5. 数字范围渲染</h2>
        <p>
          <code>v-for</code>
          可以接受整数。在这种情况下，它会把模板重复对应次数，基于
          <code>1...n</code>
          的取值范围。
        </p>

        <div class="code-block">
          <textarea readonly>
<!-- 渲染1到10的数字 -->
<span v-for="n in 10" :key="n" style="margin-right: 10px">
  {{ n }}
</span>

<!-- 创建表格 -->
<table>
  <tr v-for="row in 5" :key="row">
    <td v-for="col in 5" :key="col">
      {{ row }}-{{ col }}
    </td>
  </tr>
</table>
          </textarea>
        </div>

        <div class="demo-section" id="number-range-demo">
          <h3>实际演示</h3>
          <h4>数字序列：</h4>
          <div style="margin: 10px 0;">
            <span v-for="n in 10" :key="n" style="margin-right: 10px; padding: 5px 10px; background: #e9ecef; border-radius: 3px;">
              {{ n }}
            </span>
          </div>
          
          <h4>简单表格：</h4>
          <table style="border-collapse: collapse; margin: 10px 0;">
            <tr v-for="row in 3" :key="row">
              <td v-for="col in 4" :key="col" style="border: 1px solid #ddd; padding: 8px; text-align: center;">
                {{ row }}-{{ col }}
              </td>
            </tr>
          </table>
          
          <div class="output">
            <strong>用途：</strong>
            常用于创建网格布局、分页器、评分组件等
          </div>
        </div>
      </div>

      <!-- 6. 嵌套循环 -->
      <div class="container">
        <h2>6. 嵌套循环</h2>
        <p>
          在复杂的数据结构中，我们经常需要使用嵌套的
          <code>v-for</code>
          来渲染多层级的列表。
        </p>

        <div class="code-block">
          <textarea readonly>
<div v-for="category in categories" :key="category.name">
  <h4>{{ category.name }}</h4>
  <ul>
    <li v-for="item in category.items" :key="item">
      {{ item }}
    </li>
  </ul>
</div>

<!-- 对应的数据 -->
const categories = ref([
  {
    name: '水果',
    items: ['苹果', '香蕉', '橙子']
  },
  {
    name: '蔬菜',
    items: ['白菜', '萝卜', '土豆']
  }
])
          </textarea>
        </div>

        <div class="demo-section" id="nested-loop-demo">
          <h3>实际演示</h3>
          <div v-for="category in categories" :key="category.name" class="category-section">
            <h4 style="color: #2980b9; margin: 0 0 10px 0;">{{ category.name }}</h4>
            <ul style="margin: 0; padding-left: 20px;">
              <li v-for="item in category.items" :key="item" style="margin: 5px 0;">
                {{ item }}
              </li>
            </ul>
          </div>
          
          <div class="output">
            <strong>注意：</strong>
            嵌套循环时要确保每层的key都是唯一的
          </div>
        </div>
      </div>

      <!-- 7. 动态列表操作 -->
      <div class="container">
        <h2>7. 动态列表操作</h2>
        <p>
          Vue能够检测数组的变更并自动更新视图。支持的变更方法包括：
          <code>push()</code>、<code>pop()</code>、<code>shift()</code>、<code>unshift()</code>、
          <code>splice()</code>、<code>sort()</code>、<code>reverse()</code>等。
        </p>

        <div class="code-block">
          <textarea readonly>
<div>
  <input 
    v-model="newItem" 
    @keyup.enter="addItem" 
    placeholder="输入新项目" 
  />
  <button @click="addItem">添加</button>
  <button @click="shuffleList">随机排序</button>
  <button @click="reverseList">反转列表</button>
  <button @click="clearList">清空列表</button>

  <div v-for="(item, index) in dynamicList" :key="item.id" class="dynamic-item">
    <span>{{ item.text }}</span>
    <button @click="removeItem(index)">删除</button>
  </div>
</div>

<!-- 对应的方法 -->
const addItem = () => {
  if (newItem.value.trim()) {
    dynamicList.value.push({
      id: nextId++,
      text: newItem.value.trim()
    })
    newItem.value = ''
  }
}

const removeItem = (index) => {
  dynamicList.value.splice(index, 1)
}
          </textarea>
        </div>

        <div class="demo-section" id="dynamic-list-demo">
          <h3>实际演示</h3>
          <div style="margin-bottom: 15px;">
            <input 
              v-model="newItem" 
              @keyup.enter="addItem" 
              placeholder="输入新项目" 
              style="margin-right: 10px;"
            />
            <button @click="addItem">添加</button>
            <button @click="shuffleList">随机排序</button>
            <button @click="reverseList">反转列表</button>
            <button @click="clearList">清空列表</button>
          </div>

          <div v-for="(item, index) in dynamicList" :key="item.id" class="dynamic-item">
            <span>{{ item.text }}</span>
            <button @click="removeItem(index)">删除</button>
          </div>
          
          <div class="output">
            <strong>列表长度：</strong>
            {{ dynamicList.length }}
            <br>
            <strong>操作说明：</strong>
            支持添加、删除、排序等操作，Vue会自动更新视图
          </div>
        </div>
      </div>

      <!-- 8. key属性的重要性 -->
      <div class="container">
        <h2>8. key属性的重要性</h2>
        <p>
          当Vue正在更新使用
          <code>v-for</code>
          渲染的元素列表时，它默认使用"就地更新"的策略。如果数据项的顺序被改变，Vue将不会移动DOM元素来匹配数据项的顺序，而是就地更新每个元素。
        </p>

        <div class="performance-note">
          <h4>为什么需要key？</h4>
          <ul>
            <li><strong>性能优化：</strong>帮助Vue识别哪些元素改变了，避免不必要的DOM操作</li>
            <li><strong>状态保持：</strong>确保组件状态正确对应到数据项</li>
            <li><strong>动画效果：</strong>为列表项的添加、删除提供平滑的过渡效果</li>
          </ul>
          
          <h4>key的选择原则：</h4>
          <ul>
            <li>使用唯一且稳定的标识符（如id）</li>
            <li>避免使用数组索引作为key（当列表会发生变化时）</li>
            <li>确保同一列表中的key不重复</li>
          </ul>
        </div>

        <div class="code-block">
          <textarea readonly>
<!-- 好的做法：使用唯一ID -->
<div v-for="user in users" :key="user.id">
  {{ user.name }}
</div>

<!-- 避免：使用索引（当列表会变化时） -->
<div v-for="(user, index) in users" :key="index">
  {{ user.name }}
</div>

<!-- 可以接受：静态列表使用索引 -->
<div v-for="(color, index) in staticColors" :key="index">
  {{ color }}
</div>
          </textarea>
        </div>
      </div>

      <!-- 9. 性能优化和最佳实践 -->
      <div class="container">
        <h2>9. 性能优化和最佳实践</h2>
        
        <div class="info">
          <h4>最佳实践：</h4>
          <ol>
            <li><strong>合理使用key：</strong>为每个列表项提供唯一的key属性</li>
            <li><strong>避免在v-for中使用v-if：</strong>可以使用计算属性或template元素</li>
            <li><strong>大列表优化：</strong>考虑虚拟滚动或分页加载</li>
            <li><strong>数据结构优化：</strong>使用扁平化的数据结构提高渲染性能</li>
            <li><strong>避免深层嵌套：</strong>过深的嵌套会影响性能</li>
          </ol>
        </div>

        <div class="warning">
          <h4>常见陷阱：</h4>
          <ul>
            <li>在同一元素上使用v-for和v-if（v-if优先级更高）</li>
            <li>修改数组索引或长度时直接赋值（Vue 3中已解决）</li>
            <li>在嵌套循环中使用相同的key值</li>
            <li>忘记为动态列表项添加key属性</li>
          </ul>
        </div>
      </div>

      <!-- 总结 -->
      <div class="container">
        <h2>总结</h2>
        <div class="info">
          <h3>关键要点：</h3>
          <ol>
            <li>
              <strong>基础语法：</strong>
              掌握v-for的各种用法：数组、对象、数字范围
            </li>
            <li>
              <strong>key属性：</strong>
              始终为列表项提供唯一的key，优化渲染性能
            </li>
            <li>
              <strong>数组变更：</strong>
              了解Vue能检测的数组变更方法
            </li>
            <li>
              <strong>嵌套循环：</strong>
              正确处理多层级数据结构的渲染
            </li>
            <li>
              <strong>性能优化：</strong>
              遵循最佳实践，避免常见性能陷阱
            </li>
            <li>
              <strong>动态操作：</strong>
              实现列表的增删改查功能
            </li>
          </ol>
        </div>
      </div>

    <script>
      const { createApp, ref } = Vue

      // 1. 基础数组渲染示例
      createApp({
        setup() {
          const fruits = ref(['苹果', '香蕉', '橙子', '葡萄'])
          
          return {
            fruits
          }
        }
      }).mount('#basic-array-demo')

      // 2. 带索引的数组渲染示例
      createApp({
        setup() {
          const colors = ref(['红色', '绿色', '蓝色', '黄色'])
          
          return {
            colors
          }
        }
      }).mount('#indexed-array-demo')

      // 3. 对象数组渲染示例
      createApp({
        setup() {
          const students = ref([
            { id: 1, name: '张三', score: 85 },
            { id: 2, name: '李四', score: 92 },
            { id: 3, name: '王五', score: 78 },
            { id: 4, name: '赵六', score: 55 }
          ])

          const updateScore = studentId => {
            const student = students.value.find(s => s.id === studentId)
            if (student) {
              student.score = Math.min(100, student.score + 10)
            }
          }
          
          return {
            students,
            updateScore
          }
        }
      }).mount('#object-array-demo')

      // 4. 对象属性渲染示例
      createApp({
        setup() {
          const userInfo = ref({
            姓名: '张三',
            年龄: 25,
            职业: '前端开发',
            城市: '北京'
          })
          
          return {
            userInfo
          }
        }
      }).mount('#object-props-demo')

      // 5. 数字范围渲染示例
      createApp({
        setup() {
          return {}
        }
      }).mount('#number-range-demo')

      // 6. 嵌套循环示例
      createApp({
        setup() {
          const categories = ref([
            {
              name: '水果',
              items: ['苹果', '香蕉', '橙子']
            },
            {
              name: '蔬菜',
              items: ['白菜', '萝卜', '土豆']
            },
            {
              name: '肉类',
              items: ['猪肉', '牛肉', '鸡肉']
            }
          ])
          
          return {
            categories
          }
        }
      }).mount('#nested-loop-demo')

      // 7. 动态列表操作示例
      createApp({
        setup() {
          const newItem = ref('')
          const dynamicList = ref([
            { id: 1, text: '项目1' },
            { id: 2, text: '项目2' },
            { id: 3, text: '项目3' }
          ])

          let nextId = 4

          const addItem = () => {
            if (newItem.value.trim()) {
              dynamicList.value.push({
                id: nextId++,
                text: newItem.value.trim()
              })
              newItem.value = ''
            }
          }

          const removeItem = index => {
            dynamicList.value.splice(index, 1)
          }

          const shuffleList = () => {
            for (let i = dynamicList.value.length - 1; i > 0; i--) {
              const j = Math.floor(Math.random() * (i + 1))
              ;[dynamicList.value[i], dynamicList.value[j]] = [
                dynamicList.value[j],
                dynamicList.value[i]
              ]
            }
          }

          const reverseList = () => {
            dynamicList.value.reverse()
          }

          const clearList = () => {
            dynamicList.value = []
          }
          
          return {
            newItem,
            dynamicList,
            addItem,
            removeItem,
            shuffleList,
            reverseList,
            clearList
          }
        }
      }).mount('#dynamic-list-demo')
    </script>
  </body>
</html>